{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "369c3444",
   "metadata": {},
   "source": [
    "# ReadtheDocs Retrieval Augmented Generation (RAG) using Milvus Client"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f6ffd11a",
   "metadata": {},
   "source": [
    "In this notebook, we are going to use Milvus documentation pages to create a chatbot about our product.\n",
    "\n",
    "A chatbot is going to follow RAG steps to retrieve chunks of data using Semantic Vector Search, then the Question + Context will be fed as a Prompt to a LLM to generate an answer.\n",
    "\n",
    "<div>\n",
    "<img src=\"../../../images/rag_image.png\" width=\"80%\"/>\n",
    "</div>\n",
    "\n",
    "Many RAG demos use OpenAI for the Embedding Model and ChatGPT for the Generative AI model.  In this notebook, we will demo a fully open source RAG stack - open source embedding model available on HuggingFace, Milvus, and an open source LLM.\n",
    "\n",
    "Let's get started!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "d7570b2e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# For colab install these libraries in this order:\n",
    "# !pip install milvus, pymilvus, langchain, torch, transformers, python-dotenv, accelerate\n",
    "\n",
    "# Import common libraries.\n",
    "import time\n",
    "import pandas as pd\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e059b674",
   "metadata": {},
   "source": [
    "## Download Milvus documentation to a local directory."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "20dcdaf7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# # Uncomment to download readthedocs page locally.\n",
    "\n",
    "# DOCS_PAGE=\"https://pymilvus.readthedocs.io/en/latest/\"\n",
    "# !echo $DOCS_PAGE\n",
    "\n",
    "# # Specify encoding to handle non-unicode characters in documentation.\n",
    "# !wget -r -A.html -P rtdocs --header=\"Accept-Charset: UTF-8\" $DOCS_PAGE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a67e382",
   "metadata": {},
   "source": [
    "## Start up a local Milvus server."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb844837",
   "metadata": {},
   "source": [
    "Code in this notebook uses [Milvus client](https://milvus.io/docs/using_milvusclient.md) with [Milvus lite](https://milvus.io/docs/milvus_lite.md), which runs a local server.  ⛔️ Milvus lite is only meant for demos and local testing.\n",
    "- pip install milvus pymilvus\n",
    "\n",
    "💡 **For production purposes**, use a local Milvus docker, Milvus clusters, or fully-managed Milvus on Zilliz Cloud.\n",
    "- [Local Milvus docker](https://milvus.io/docs/install_standalone-docker.md) requires local docker installed and running.\n",
    "- [Milvus clusters](https://milvus.io/docs/install_cluster-milvusoperator.md) requires a K8s cluster up and running.\n",
    "- [Ziliz Cloud free trial](https://cloud.zilliz.com/login) choose a \"free\" option when you provision.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "0806d2db",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Milvus server startup time: 6.501969814300537 sec\n",
      "v2.2-testing-20230824-68-ga34a9d606-lite\n"
     ]
    }
   ],
   "source": [
    "from milvus import default_server\n",
    "from pymilvus import (\n",
    "    connections, utility, \n",
    "    MilvusClient,\n",
    ")\n",
    "\n",
    "# Cleanup previous data and stop server in case it is still running.\n",
    "default_server.stop()\n",
    "default_server.cleanup()\n",
    "\n",
    "# Start a new milvus-lite local server.\n",
    "start_time = time.time()\n",
    "default_server.start()\n",
    "\n",
    "end_time = time.time()\n",
    "print(f\"Milvus server startup time: {end_time - start_time} sec\")\n",
    "# startup time: 5.6739208698272705\n",
    "\n",
    "# Add wait to avoid error message from trying to connect.\n",
    "time.sleep(15)\n",
    "\n",
    "# Now you could connect with localhost and the given port.\n",
    "# Port is defined by default_server.listen_port.\n",
    "connections.connect(host='127.0.0.1', \n",
    "                  port=default_server.listen_port,\n",
    "                  show_startup_banner=True)\n",
    "\n",
    "# Check if the server is ready.\n",
    "print(utility.get_server_version())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b01d6622",
   "metadata": {},
   "source": [
    "## Load the Embedding Model checkpoint and use it to create vector embeddings\n",
    "**Embedding model:**  We will use the open-source [sentence transformers](https://www.sbert.net/docs/pretrained_models.html) available on HuggingFace to encode the documentation text.  We will download the model from HuggingFace and run it locally.  We'll save the model's generated embeedings to a pandas dataframe and then into the milvus database.\n",
    "\n",
    "💡 Note:  To keep your tokens private, best practice is to use an env variable.   <br>\n",
    "In Jupyter, need .env file (in same dir as notebooks) containing lines like this:\n",
    "- VARIABLE_NAME=value"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "dd2be7fd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "device: cpu\n",
      "<class 'sentence_transformers.SentenceTransformer.SentenceTransformer'>\n",
      "SentenceTransformer(\n",
      "  (0): Transformer({'max_seq_length': 512, 'do_lower_case': True}) with Transformer model: BertModel \n",
      "  (1): Pooling({'word_embedding_dimension': 768, 'pooling_mode_cls_token': True, 'pooling_mode_mean_tokens': False, 'pooling_mode_max_tokens': False, 'pooling_mode_mean_sqrt_len_tokens': False})\n",
      ")\n",
      "model_name: BAAI/bge-base-en-v1.5\n",
      "EMBEDDING_LENGTH: 768\n",
      "MAX_SEQ_LENGTH: 512\n"
     ]
    }
   ],
   "source": [
    "# Import torch.\n",
    "import torch\n",
    "from torch.nn import functional as F\n",
    "from sentence_transformers import SentenceTransformer\n",
    "\n",
    "# Initialize torch settings\n",
    "torch.backends.cudnn.deterministic = True\n",
    "RANDOM_SEED = 415\n",
    "torch.manual_seed(RANDOM_SEED)\n",
    "DEVICE = torch.device('cuda:3' if torch.cuda.is_available() else 'cpu')\n",
    "print(f\"device: {DEVICE}\")\n",
    "\n",
    "import os\n",
    "from dotenv import load_dotenv, find_dotenv\n",
    "_ = load_dotenv(find_dotenv())\n",
    "from huggingface_hub import login\n",
    "\n",
    "# # Login to huggingface_hub\n",
    "# hub_token = os.getenv(\"HUGGINGFACEHUB_API_TOKEN\")\n",
    "# login(token=hub_token)\n",
    "\n",
    "# Load the model from huggingface model hub.\n",
    "model_name = \"BAAI/bge-base-en-v1.5\"\n",
    "retriever = SentenceTransformer(model_name, device=DEVICE)\n",
    "print(type(retriever))\n",
    "print(retriever)\n",
    "\n",
    "# Get the model parameters and save for later.\n",
    "MAX_SEQ_LENGTH = retriever.get_max_seq_length() \n",
    "HF_EOS_TOKEN_LENGTH = 1\n",
    "EMBEDDING_LENGTH = retriever.get_sentence_embedding_dimension()\n",
    "\n",
    "# Inspect model parameters.\n",
    "print(f\"model_name: {model_name}\")\n",
    "print(f\"EMBEDDING_LENGTH: {EMBEDDING_LENGTH}\")\n",
    "print(f\"MAX_SEQ_LENGTH: {MAX_SEQ_LENGTH}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "dd543a62",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "langchain.embeddings.huggingface.HuggingFaceEmbeddings"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Convert the HuggingFace embeddings to a Langchain embeddings.\n",
    "from langchain.embeddings import HuggingFaceEmbeddings\n",
    "\n",
    "model_kwargs = {\"device\": DEVICE}\n",
    "encode_kwargs = {'normalize_embeddings': True}\n",
    "lc_encoder = HuggingFaceEmbeddings(\n",
    "    model_name=model_name,\n",
    "    model_kwargs=model_kwargs,\n",
    "    encode_kwargs=encode_kwargs\n",
    ")\n",
    "type(lc_encoder)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "6861beb7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loaded 15 documents\n"
     ]
    }
   ],
   "source": [
    "## Read docs into LangChain\n",
    "#!pip install langchain \n",
    "from langchain.document_loaders import ReadTheDocsLoader\n",
    "\n",
    "loader = ReadTheDocsLoader(\"rtdocs/pymilvus.readthedocs.io/en/latest/\", features=\"html.parser\")\n",
    "docs = loader.load()\n",
    "\n",
    "num_documents = len(docs)\n",
    "print(f\"loaded {num_documents} documents\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c60423a5",
   "metadata": {},
   "source": [
    "## Chunking\n",
    "\n",
    "Before embedding, it is necessary to decide your chunk strategy, chunk size, and chunk overlap.  In this demo, I will use:\n",
    "- **Strategy** = Naive for now.  TODO use markdown header hierarchies.\n",
    "- **Chunk size** = Use the embedding model's parameter `MAX_SEQ_LENGTH`\n",
    "- **Overlap** = Rule-of-thumb 10-15%\n",
    "- **Function** = Langchain's convenient `RecursiveCharacterTextSplitter` to split up long reviews recursively.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "a53595fa",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "chunking time: 0.0025839805603027344\n",
      "type: list of <class 'langchain.schema.document.Document'>, len: 197\n",
      "\n",
      "Looking at a sample chunk...\n",
      "{'source': 'rtdocs/pymilvus.readthedocs.io/en/latest/install.html'}\n",
      "Installation¶\n",
      "Installing via pip¶\n",
      "PyMilvus is in the Python Package Index.\n",
      "PyMilvus only support pyt\n"
     ]
    }
   ],
   "source": [
    "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
    "\n",
    "# Use the embedding model parameters to calculate chunk_size and overlap.\n",
    "chunk_size = MAX_SEQ_LENGTH - HF_EOS_TOKEN_LENGTH\n",
    "# Default chunk overlap is 10% chunk_size.\n",
    "chunk_overlap = np.round(chunk_size * 0.10, 0)\n",
    "\n",
    "# Use recursive splitter to chunk text.\n",
    "start_time = time.time()\n",
    "text_splitter = RecursiveCharacterTextSplitter(\n",
    "    chunk_size = chunk_size,\n",
    "    chunk_overlap = chunk_overlap,\n",
    "    length_function = len,\n",
    ")\n",
    "\n",
    "chunks = text_splitter.create_documents(\n",
    "    [doc.page_content for doc in docs], \n",
    "    metadatas=[doc.metadata for doc in docs])\n",
    "\n",
    "end_time = time.time()\n",
    "print(f\"chunking time: {end_time - start_time}\")\n",
    "# print(f\"type: {type(chunks)}, len: {len(chunks)}, type: {type(chunks[0])}\")\n",
    "print(f\"type: list of {type(chunks[0])}, len: {len(chunks)}\") \n",
    "\n",
    "print()\n",
    "print(\"Looking at a sample chunk...\")\n",
    "print(chunks[0].metadata)\n",
    "print(chunks[0].page_content[:100])\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "512130a3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'source': 'https://pymilvus.readthedocs.io/en/latest/install.html'}\n",
      "Installation¶\n",
      "Installing via pip¶\n",
      "PyMilvus is in the Python Package Index.\n",
      "PyMilvus only support python3(>= 3.6), usually, it’s ok to install PyMilvus like below.\n",
      "$ python3 -m pip install pymilvus\n",
      "Installing in a virtual environment¶\n",
      "It’s recommended to use PyMilvus in a virtual environment, using virtual environment allows you to avoid\n",
      "installing Python packages globally which could break system tools or other projects.\n"
     ]
    }
   ],
   "source": [
    "# Clean up the metadata urls\n",
    "for doc in chunks:\n",
    "    new_url = doc.metadata[\"source\"]\n",
    "    new_url = new_url.replace(\"rtdocs\", \"https:/\")\n",
    "    doc.metadata.update({\"source\": new_url})\n",
    "\n",
    "print(chunks[0].metadata)\n",
    "print(chunks[0].page_content[:500])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d9bd8153",
   "metadata": {},
   "source": [
    "## Insert data into Milvus\n",
    "\n",
    "The code below uses the [Langchain Milvus](https://api.python.langchain.com/en/latest/_modules/langchain/vectorstores/milvus.html#Milvus) adapter.  \n",
    "- Default index is AUTOINDEX. <br>\n",
    "💡 AUTOINDEX works on both Milvus and Zilliz Cloud (where it is the fastest!)\n",
    "- collection_name is \"LangChainCollection\".\n",
    "- Schema is \n",
    "  - pk (str): Name of the primary key field.\n",
    "  - text (str): Name of the text field.\n",
    "  - vector (str): Name of the vector field. \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "b51ff139",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Start inserting entities\n",
      "Langchain Milvus insert time for 197 vectors: 11.850640058517456 seconds\n",
      "type: <class 'langchain.vectorstores.milvus.Milvus'>\n"
     ]
    }
   ],
   "source": [
    "# Insert a batch of data into the Milvus collection.\n",
    "from langchain.vectorstores import Milvus\n",
    "MILVUS_PORT = 19530\n",
    "MILVUS_HOST = \"127.0.0.1\"\n",
    "\n",
    "print(\"Start inserting entities\")\n",
    "start_time = time.time()\n",
    "\n",
    "vector_store = Milvus.from_documents(\n",
    "    chunks,\n",
    "    embedding=lc_encoder,\n",
    "    connection_args={\"host\": MILVUS_HOST, \n",
    "                     \"port\": MILVUS_PORT},\n",
    ")\n",
    "\n",
    "end_time = time.time()\n",
    "print(f\"Langchain Milvus insert time for {len(chunks)} vectors: {end_time - start_time} seconds\")\n",
    "print(f\"type: {type(vector_store)}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4ebfb115",
   "metadata": {},
   "source": [
    "## Run a Semantic Search\n",
    "\n",
    "Now we can search all the documentation embeddings to find the `TOP_K` documentation chunks with the closest embeddings to a user's query.\n",
    "- In this example, we'll ask about AUTOINDEX.\n",
    "\n",
    "💡 The same model should always be used for consistency for all the embeddings."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02c589ff",
   "metadata": {},
   "source": [
    "## Ask a question about your data\n",
    "\n",
    "So far in this demo notebook: \n",
    "1. Your custom data has been mapped into a vector embedding space\n",
    "2. Those vector embeddings have been saved into a vector database\n",
    "\n",
    "Next, you can ask a question about your custom data!\n",
    "\n",
    "💡 In LLM lingo:\n",
    "> **Query** is the generic term for user questions.  \n",
    "A query is a list of multiple individual questions, up to maybe 1000 different questions!\n",
    "\n",
    "> **Question** usually refers to a single user question.  \n",
    "In our example below, the user question is \"What is AUTOINDEX in Milvus Client?\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "5e7f41f4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "query length: 47\n"
     ]
    }
   ],
   "source": [
    "# Define a sample question about your data.\n",
    "question = 'What is the default AUTOINDEX in Milvus Client?'\n",
    "query = [question]\n",
    "\n",
    "# Inspect the length of the query.\n",
    "QUERY_LENGTH = len(query[0])\n",
    "print(f\"query length: {QUERY_LENGTH}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9ea29411",
   "metadata": {},
   "source": [
    "## Execute a vector search\n",
    "\n",
    "Search Milvus using [PyMilvus API](https://milvus.io/docs/search.md).\n",
    "\n",
    "💡 By their nature, vector searches are \"semantic\" searches.  For example, if you were to search for \"leaky faucet\": \n",
    "> **Traditional Key-word Search** - either or both words \"leaky\", \"faucet\" would have to match some text in order to return a web page or link text to the document.\n",
    "\n",
    "> **Semantic search** - results containing words \"drippy\" \"taps\" would be returned as well because these words mean the same thing even though they are different words,"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "89642119",
   "metadata": {},
   "outputs": [],
   "source": [
    "# RETRIEVAL USING MILVUS WITH LANGCHAIN\n",
    "\n",
    "# Search with metadata.  TODO: Add better filtering query!\n",
    "METADATA_URL = \"https://pymilvus.readthedocs.io/en/latest/_modules/milvus/client/stub.html\"\n",
    "SEARCH_PARAMS = dict({\n",
    "    \"expr\": \"text = METADATA_URL\",\n",
    "    })\n",
    "\n",
    "start_time = time.time()\n",
    "\n",
    "# Default search.\n",
    "docs = vector_store.similarity_search(\n",
    "    question, \n",
    "    k=100,\n",
    "    param=SEARCH_PARAMS,\n",
    "    verbose=True,\n",
    "    )\n",
    "\n",
    "end_time = time.time()\n",
    "print(f\"Milvus query time: {end_time - start_time}\")\n",
    "\n",
    "# # View the retrieval result.\n",
    "# for d in docs:\n",
    "#     print(d.metadata)\n",
    "#     # print(d.page_content[:100])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "95f7e011",
   "metadata": {},
   "source": [
    "## Assemble and inspect the search result\n",
    "\n",
    "The search result is in the list variable `docs` of type `'pymilvus.orm.search.SearchResult'`.  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "d3dfa33a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Count raw retrievals: 100\n",
      "Count unique texts: 37\n",
      "17084\n"
     ]
    }
   ],
   "source": [
    "print(f\"Count raw retrievals: {len(docs)}\")\n",
    "\n",
    "unique_sources = []\n",
    "unique_texts = []\n",
    "for doc in docs:\n",
    "    if doc.metadata['source'] == METADATA_URL:\n",
    "        if doc.page_content not in unique_texts:\n",
    "            unique_texts.append(doc.page_content)\n",
    "            unique_sources.append(doc.metadata['source'])\n",
    "print(f\"Count unique texts: {len(unique_texts)}\")\n",
    "# [ print(text) for text in unique_texts ]\n",
    "\n",
    "# Assemble all the results in a zipped list.\n",
    "formatted_context = list(zip(unique_sources, unique_texts))\n",
    "\n",
    "# Assemble the context as a stuffed string.\n",
    "context = \"\"\n",
    "for source, text in formatted_context:\n",
    "    context += f\"{text} \"\n",
    "print(len(context))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd6060ce",
   "metadata": {},
   "source": [
    "## Use an LLM to Generate a chat response to the user's question using the Retrieved Context.\n",
    "\n",
    "Below, we're using an open, very tiny generative AI model, or LLM.  Many demos use OpenAI as the LLM choice instead."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "3e7fa0b6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Question: What is the default AUTOINDEX in Milvus Client?\n",
      "Answer: lazy dog\n"
     ]
    }
   ],
   "source": [
    "# BASELINING THE LLM: ASK A QUESTION WITHOUT ANY RETRIEVED CONTEXT.\n",
    "\n",
    "from transformers import AutoModelForQuestionAnswering, AutoTokenizer, pipeline\n",
    "\n",
    "# Load the Hugging Face auto-regressive LLM checkpoint.\n",
    "llm = \"deepset/tinyroberta-squad2\"\n",
    "tokenizer = AutoTokenizer.from_pretrained(llm)\n",
    "\n",
    "# context cannot be empty so just put random text in it.\n",
    "QA_input = {\n",
    "    'question': question,\n",
    "    'context': 'The quick brown fox jumped over the lazy dog'\n",
    "}\n",
    "\n",
    "nlp = pipeline('question-answering', \n",
    "               model=llm, \n",
    "               tokenizer=tokenizer)\n",
    "\n",
    "result = nlp(QA_input)\n",
    "print(f\"Question: {question}\")\n",
    "print(f\"Answer: {result['answer']}\")\n",
    "\n",
    "# The baseline LLM chat is not very helpful."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "a68e87b1",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Question: What is the default AUTOINDEX in Milvus Client?\n",
      "Answer: MetricType.L2\n"
     ]
    }
   ],
   "source": [
    "# NOW ASK THE SAME LLM THE SAME QUESTION USING THE RETRIEVED CONTEXT.\n",
    "QA_input = {\n",
    "    'question': question,\n",
    "    'context': context,\n",
    "}\n",
    "\n",
    "nlp = pipeline('question-answering', \n",
    "               model=llm, \n",
    "               tokenizer=tokenizer)\n",
    "\n",
    "result = nlp(QA_input)\n",
    "print(f\"Question: {question}\")\n",
    "print(f\"Answer: {result['answer']}\")\n",
    "\n",
    "# That answer looks a little better!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "d0e81e68",
   "metadata": {},
   "outputs": [],
   "source": [
    "# # Shut down and cleanup the milvus server.\n",
    "default_server.stop()\n",
    "default_server.cleanup()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "c777937e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Author: Christy Bergman\n",
      "\n",
      "Python implementation: CPython\n",
      "Python version       : 3.10.12\n",
      "IPython version      : 8.15.0\n",
      "\n",
      "torch       : 2.0.1\n",
      "transformers: 4.34.1\n",
      "milvus      : 2.3.0\n",
      "pymilvus    : 2.3.0\n",
      "langchain   : 0.0.301\n",
      "\n",
      "conda environment: py310\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Props to Sebastian Raschka for this handy watermark.\n",
    "# !pip install watermark\n",
    "\n",
    "%load_ext watermark\n",
    "%watermark -a 'Christy Bergman' -v -p torch,transformers,milvus,pymilvus,langchain --conda"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
